package dungeon;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import basicPackage.Coordinate;
import basicPackage.Direction;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A generic class representing a dungeon containing elements their respective coordinate.
 * 
 * @invar	| getMaxCoordinate != null
 * @invar	| getMaxCoordinate().isPositive
 * @invar	| if(getSuperCompositedungeon != null)
 * 			|	getMaxCoordinate().add(getDungeonCoordinate()).isSmallerOrEqualTo(getSuperCompositeDungeon().geMaxCoordinate
 * @invar	| getSuperCompositeDungeon() != this
 * @invar	| (getDungeonCoordinate() == null || getDungeonCoordinate().isPositive())
 * @invar	| if(containsElementCoordinate(coordinate)
 * 			|	coordinate.isPositive && coordinate.isSmallerOrEqualTo(getMaxCoordinate()) && !coordinate.hasSameValueForAllCoordinates()
 * @invar	| if( exists coordinate: getElement(coordinate) != null)
 * 			|	exists no coordinate2: coordinate2 != coordinate && getElement(coordinate) == getElement(coordinate2)
 * @invar	| if( exists element: exists coordinate: getElement(coordinate) == element))
 * 			|	exists no element2: element2 != element: getElement(coordinate) == element2
 * @invar	| if( exists coordinate: getSquare(coordinate) != null)
 * 			|	exists no coordinate2: coordinate2 != coordinate && getSquare(coordinate) == getSquare(coordinate2)
 * @invar	| if( exists square: exists coordinate: getSquare(coordinate) == square))
 * 			|	exists no square2: square != square2: getSquare(coordinate) == square2
 * @invar	| if(containsSquareCoordinate(coordinate))
 * 			|	for each Direction.i: containsSquareCoordinate(coordinate.getNeighboringCoordinates().get(i))
 * 			|		getSquare(coordinate).getBorderingSquare(i) == getSquare(coordinate.getNeighboringCoordinates().get(i))
 * 			|		getSquare(coordinate.getNeighboringCoordinates().get(i)).getBorderingSquare(i.oppositeDirection()) == getSquare(coordinate)
 * 
 * @author Laurens Vanderstraeten
 */
public abstract class Dungeon<E> {
	
	/**
	 * Constructs a dungeon.
	 * 
	 * @param 	maxCoordinate
	 * 			The maximum coordinate determining the outer point in the cuboid space of 
	 * 			possible square coordinates.
	 * @post	| getMaxCoordinate() == maxCoordinate
	 * @throws	IllegalArgumentException
	 * 			| !isValidMaxCoordinate(maxCoordinate)
	 */
	@Raw
	protected Dungeon(Coordinate maxCoordinate) throws IllegalArgumentException {
		setMaxCoordinate(maxCoordinate);
	}
	
	/**
	 * Return the maximum coordinate determining the outer point in the cuboid space of 
	 * possible coordinates.
	 */
	@Basic @Raw
	public Coordinate getMaxCoordinate() {
		return maxCoordinate;
	}
	
	/**
	 * Check whether the given coordinate is a valid maximum coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate to be checked.
	 * @return	| if(coordinate == null)
	 *			|	result == false
	 *			| if(!coordinate.isPositive())
	 *			|	result == false
	 *			| if(!coordinate.isLargerOrEqualTo(getMaxCoordinate()))
	 *			|	result == false
	 *			| if(getSuperCompositeDungeon() != null && !coordinate.add(getDungeonCoordinate()).isSmallerOrEqualTo(getSuperCompositeDungeon().getMaxCoordinate()))
	 *			|	result == false
	 *			| else then
	 *			|	result == true
	 */
	@Raw
	public Boolean isValidMaxCoordinate(Coordinate coordinate) {
		if(coordinate == null)
			return false;
		if(!coordinate.isPositive())
			return false;
		if(!coordinate.isLargerOrEqualTo(getMaxCoordinate()))
			return false;
		if(getSuperCompositeDungeon() != null && !coordinate.add(getDungeonCoordinate()).isSmallerOrEqualTo(getSuperCompositeDungeon().getMaxCoordinate()))
			return false;
		return true;
	}
	
	/**
	 * Set the maximum coordinate to the given coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate to be set as the maximum coordinate.
	 * @post	| getMaxCoordinate() == coordinate
	 * @throws 	IllegalArgumentException
	 * 			| !isValidMaxSquareCoordinate(coordinate)
	 */
	@Raw
	public void setMaxCoordinate(Coordinate coordinate) throws IllegalArgumentException{
		if(!isValidMaxCoordinate(coordinate)) {
			throw new IllegalArgumentException();
		}
		this.maxCoordinate = coordinate;
	}
	
	/**
	 * Variable for registering the maximum square coordinate determining the outer point in the 
	 * cuboid space of possible square coordinates.
	 */
	private Coordinate maxCoordinate = new Coordinate(0,0,0);
	
	/**
	 * Return the composite dungeon that contains this dungeon.
	 */
	@Basic @Raw
	public CompositeDungeon<?> getSuperCompositeDungeon() {
		return superCompositeDungeon;
	}
	
	/**
	 * Get the root dungeon of this dungeon.
	 * 
	 * @return	| if(getSuperCompositeDungeon() == null)
	 *			|	result == this;
	 *			| else then
	 *			|	result == getSuperCompositeDungeon().getRootCompositeDungeon();
	 */
	public Dungeon<?> getRootDungeon() {
		if(getSuperCompositeDungeon() == null)
			return this;
		return getSuperCompositeDungeon().getRootDungeon();
	}
	
	/**
	 * Check whether the given composite dungeon is a valid super composite dungeon.
	 * 
	 * @param 	superCompositeDungeon
	 * 			The composite dungeon to be checked.
	 * @return	| if(superCompositeDungeon == this)
	 *			|	result == false
	 *			| if(containsDungeon(superCompositeDungeon))
	 *			|	result == false
	 *			| else then
	 *			| 	result == true
	 *			
	 */
	@Raw
	public boolean isValidSuperCompositeDungeon(CompositeDungeon<?> superCompositeDungeon){
		if(superCompositeDungeon == this)
			return false;
		return true;
	}
	
	/**
	 * Set the super composite dungeon to the given composite dungeon.
	 * 
	 * @param 	superCompositeDungeon
	 * 			The new composite dungeon to be set.
	 * @post	| getSuperCompositeDungeon() == superCompositeDungeon
	 * @post	| if(superCompositeDungeon != null)
	 * 			|	getRootCompositeDungeon() == superCompositeDungeon.getRootCompositeDungeon()
	 * 			| else then
	 * 			|	getRootCompositeDungeon() == this
	 * @throws 	IllegalArgumentException
	 * 			| !isValidSuperCompositeDungeon(superCompositeDungeon)
	 */
	@Raw
	protected void setSuperCompositeDungeon(CompositeDungeon<?> superCompositeDungeon) throws IllegalArgumentException{
		if(!isValidSuperCompositeDungeon(superCompositeDungeon))
			throw new IllegalArgumentException();
		this.superCompositeDungeon = superCompositeDungeon;
	}
	
	/**
	 * Variable for registering the super composite dungeon.
	 */
	private CompositeDungeon<?> superCompositeDungeon;
	
	/**
	 * Return the coordinate under which this dungeon is stored in its super composite dungeon.
	 */
	@Basic @Raw
	public Coordinate getDungeonCoordinate() {
		return dungeonCoordinate;
	}
	
	/**
	 * Return the world coordinate in the root dungeon of the given coordinate expressed as a relative coordinate in this dungeon.
	 * 
	 * @param 	coordinate
	 * 			The coordinate expressed as a relative coordinate in this dungeon.
	 * @return	| if(getSuperCompositeDungeon() == null)
	 *			|	result == coordinate
	 *			| else then
	 *			|	result == getSuperCompositeDungeon().getWorldCoordinateInRootDungeon(coordinate.add(getDungeonCoordinate()))
	 * @throws	IllegalArgumentException
	 * 			| coordinate == null
	 */
	public Coordinate getWorldCoordinateInRootDungeon(Coordinate coordinate) throws IllegalArgumentException {
		if(coordinate == null)
			throw new IllegalArgumentException();
		if(getSuperCompositeDungeon() == null)
			return coordinate;
		return getSuperCompositeDungeon().getWorldCoordinateInRootDungeon(coordinate.add(getDungeonCoordinate()));
	}
	
	/**
	 * Return the world coordinate in the root dungeon to this dungeon's origin.
	 * 
	 * @return	| getWorldCoordinateFromRootDungeon(new Coordinate())
	 */
	public Coordinate getWorldCoordinateInRootDungeon() {
		return getWorldCoordinateInRootDungeon(new Coordinate());
	}
	
	/**
	 * Check whether the given coordinate is a valid dungeon coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate to be checked.
	 * @return	| if(coordinate != null && !coordinate.isPositive())
	 *			|	result == false
	 *			| else then
	 *			|	result == true
	 */
	@Raw
	public static Boolean isValidDungeonCoordinate(Coordinate coordinate) {
		if(coordinate != null && !coordinate.isPositive())
			return false;
		return true;
	}
	
	/**
	 * Set the dungeon coordinate to the given dungeon coordinate.
	 * 
	 * @param 	dungeonCoordinate
	 * 			The new dungeon coordinate to be set.
	 * @post	| getDungeonCoordinate() == dungeonCoordinate
	 * @throws 	IllegalArgumentException
	 * 			| !isValidDungeonCoordinate(dungeonCoordinate)
	 */
	@Raw
	protected void setDungeonCoordinate(Coordinate dungeonCoordinate) throws IllegalArgumentException {
		if(!isValidDungeonCoordinate(dungeonCoordinate))
			throw new IllegalArgumentException();
		this.dungeonCoordinate = dungeonCoordinate;
	}
	
	/**
	 * Variable for registering the dungeon coordinate.
	 */
	private Coordinate dungeonCoordinate;
	
	/**
	 * Return a Map registering all the elements under their respective coordinates.
	 */
	@Basic @Raw
	public Map<Coordinate, E> getElementMap() {
		return new HashMap<Coordinate, E>(elementMap);
	}
	
	/**
	 * Return the element registered under the given coordinate.
	 * 
	 * @param	coordinate
	 * 			The coordinate of the element to be retrieved.
	 * @return	getElementMap().get(coordinate)
	 */
	@Raw
	public E getElement(Coordinate coordinate) {
		return getElementMap().get(coordinate);
	}
	
	/**
	 * Return the square registered under the given square coordinate.
	 * 
	 * @param 	squareCoordinate
	 * 			The square coordinate of the square to be retrieved.
	 * @return	The square registered on the given coordinate or null if there is no square on the given coordinate.
	 */
	@Raw
	public abstract Square getSquare(Coordinate squareCoordinate);
	
	/**
	 * Return an Iterator iterating all the Coordinates containing elements in this dungeon.
	 * 
	 * @return 	| getElementMap().keySet().iterator()
	 */
	@Raw
	public Iterator<Coordinate> getElementCoordinateIterator() {
		return getElementMap().keySet().iterator();
	}
	
	/**
	 * Return an Iterator iterating all the elements in this dungeon.
	 * 
	 * @return	| getElementMap().values().iterator()
	 */
	@Raw
	public Iterator<E> getElementIterator() {
		return getElementMap().values().iterator();
	}
	
	/**
	 * Return an Iterator iterating all the square coordinates of all the squares within this dungeon.
	 */
	@Raw
	public abstract Iterator<Coordinate> getSquareCoordinateIterator();
	
	/**
	 * Return an Iterator iterating all the squares within this dungeon.
	 */
	@Raw
	public abstract Iterator<Square> getSquareIterator();
	
	/**
	 * Return the number of squares in this dungeon.
	 */
	@Raw
	public abstract int getNbSquares();
	
	/**
	 * Return a list with all the rocksquares in this dungeon with a temperature above 200 degrees Celsius.
	 * 
	 * @return	| for each coordinate e: containsSquareCoordinate(e)
	 * 			| for all i = getSquare(e): i instance of RockSquare && i.getTemperature().getTemperatureValue() > 200: result.add(i)
	 * 			|	result = List(i)
	 */
	public List<RockSquare> getAllRocksWithTemperatureAbove200DegreesCelsius() {
		List<RockSquare> result = new ArrayList<RockSquare>();
		Iterator<Square> squareIterator = getSquareIterator();
		while(squareIterator.hasNext()) {
			Square square = squareIterator.next();
			if(square instanceof RockSquare  && square.getTemperature().getTemperatureValue() > 200)
				result.add((RockSquare) square);
		}
		return result;
	}

	/**
	 * Check whether this dungeon contains an element on the given coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate to be checked.
	 * @return	| getElementMap().containsKey(coordinate)
	 */
	@Raw
	public Boolean containsElementCoordinate(Coordinate coordinate){
		return getElementMap().containsKey(coordinate);
	}
	
	/**
	 * Check whether this dungeon contains the given element.
	 * 
	 * @param 	element
	 * 			The element to be checked.
	 * @return	| getElementMap().containsValue(element)
	 */
	@Raw
	public Boolean containsElement(Object element) {
		return getElementMap().containsValue(element);
	}
	
	/**
	 * Check whether this dungeon contains a square on the given square coordinate.
	 * 
	 * @param 	squareCoordinate
	 * 			The square coordinate to be checked.
	 * @return	True if this dungeon contains a square on the given coordinate.
	 */
	@Raw
	public abstract Boolean containsSquareCoordinate(Coordinate squareCoordinate);
	
	/**
	 * Check whether this dungeon contains the given square.
	 * 
	 * @param 	square
	 * 			The square to be checked.
	 * @return	True if this dungeon contains the given Square.
	 */
	@Raw
	public Boolean containsSquare(Square square) {
		Iterator<Square> squareIterator = getSquareIterator();
		while(squareIterator.hasNext()){
			if(squareIterator.next() == square)
				return true;
		}
		return false;
	}
	
	/**
	 * Check whether the given coordinate in this dungeon is a free square coordinate in its root dungeon.
	 * 
	 * @param 	coordinate
	 * 			The coordinate to be checked.
	 * @return	| if(getSuperCompositeDungeon() == null)
	 *			|	result == !containsSquareCoordinate(coordinate)
	 *			| else then
	 *			|	result == getSuperCompositeDungeon().isCoordinateFreeSquareCoordinate(coordinate.add(getDungeonCoordinate()))
	 */
	@Raw
	public Boolean isCoordinateFreeWorldSquareCoordinateInRootDungeon(Coordinate coordinate) {
		if(getSuperCompositeDungeon() == null)
			return !containsSquareCoordinate(coordinate);
		return getSuperCompositeDungeon().isCoordinateFreeWorldSquareCoordinateInRootDungeon(coordinate.add(getDungeonCoordinate()));
	}
	
	/**
	 * Return the number of elements in this dungeon.
	 * 
	 * @return	getElementMap().size()
	 */
	@Raw
	public int getNbElements() {
		return getElementMap().size();
	}
	
	/**
	 * Check whether the given coordinate is a valid coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate to be checked.
	 * @return	| if(coordinate == null)
	 *			|	result == false
	 *			| if(!coordinate.isPositive())
	 *			|	result == false
	 *			| if(!coordinate.isSmallerOrEqualTo(getMaxCoordinate()))
	 *			|	result == false
	 *			| if(coordinate.hasSameValueForAllCoordinates())
	 *			|	result == false
	 *			| else then
	 *			|	result == true
	 */
	public Boolean isValidCoordinate(Coordinate coordinate) {
		if(coordinate == null)
			return false;
		if(!coordinate.isPositive())
			return false;
		if(!coordinate.isSmallerOrEqualTo(getMaxCoordinate()))
			return false;
		if(coordinate.hasSameValueForAllCoordinates())
			return false;
		return true;
	}
	
	/**
	 * Check whether the given coordinate is a valid element coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate to be checked.
	 * @return	| if(!isValidCoordinate(coordinate))
	 *			|	result == false
	 *			| if(getElementMap().keySet().contains(coordinate))
	 *			|	result == false
	 *			| else then
	 *			|	result == true
	 */
	@Raw
	public Boolean isValidElementCoordinate(Coordinate coordinate){
		if(!isValidCoordinate(coordinate))
			return false;
		if(getElementMap().keySet().contains(coordinate))
			return false;
		return true;
	}
	
	/**
	 * Check whether the given element is a valid element.
	 * 
	 * @param 	element
	 * 			The element to be checked.
	 * @return	| if(element == null)
	 *			|	result == false
	 *			| if(elementMap.values().contains(element))
	 *			|	result == false
	 *			| else then
	 *			|	result == true
	 */
	@Raw
	public Boolean isValidElement(E element) {
		if(element == null)
			return false;
		if(getElementMap().values().contains(element))
			return false;
		return true;
	}
	
	/**
	 * Set the given element in this dungeon under the given coordinate.
	 * 
	 * @param	coordinate
	 * 			The coordinate under which the element should be set.
	 * @param 	element
	 * 			The element to be set.
	 * @post	| getElementMap().get(coordinate) == element;
	 * @post	| getElement(coordinate) == element
	 * @post	| containsElementcoordinate(coordinate)
	 * @post	| containsElement(element)
	 * @post	| new.getNbElements() == old.getNbElements() + 1
	 * @post	| if(element instanceof Square)
	 * 			|	getSquare(coordinate) == element && new.getNbSquares() = old.getNbSquares + 1 && containsSquareCoordinate(coordinate) && containsSquare(element)
	 * @throws	IllegalArgumentException
	 * 			| !isValidElement(element) || !isValidElementCoordinate(coordinate)
	 */
	@Raw
	protected void setElement(Coordinate coordinate, E element) throws IllegalArgumentException{
		if(!isValidElement(element) || !isValidElementCoordinate(coordinate))
			throw new IllegalArgumentException();
		elementMap.put(coordinate, element);
	}
	
	/**
	 * Remove the element registered at the given coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate of the element to be removed.
	 * @post	| getElementMap().get(coordinate) == null;
	 * @post	| getElement(coordinate) == null
	 * @post	| !containsElementcoordinate(coordinate)
	 * @post	| !containsElement(old.getElement(coordinate))
	 * @post	| if(old.containsElementcoordinate(coordinate))
	 * 			| 	new.getNbElements() == old.getNbElements() - 1
	 * @post	| if(old.getElement(coordinate) instanceof Square)
	 * 			|	getSquare(coordinate) == null && new.getNbSquares() = old.getNbSquares - 1 && !containsSquareCoordinate(coordinate) && !containsSquare(old.getElement(coordinate))
	 */
	@Raw
	protected void removeElement(Coordinate coordinate) {
		elementMap.remove(coordinate);
	}
	
	/**
	 * Variable registering the map of elements under their respective coordinates.
	 */
	private Map<Coordinate, E> elementMap = new HashMap<Coordinate, E>();
	
	/**
	 * Resolve the connection of the square in the given coordinate to it's neighboring coordinates' squares within this dungeon.
	 * 
	 * @param 	coordinate
	 * 			The coordinate of the square to be resolved.
	 * @post	for all Direction.i:
	 * 				if(containsSquareCoordinate(coordinate.getNeighboringCoordinates().get(i)))
	 * 					getSquare(coordinate.getNeighboringCoordinates().get(i)).getBorderingSquares().contains(getSquare(coordinate))
	 * @throws	IllegalArgumentException
	 * 			coordinate == null || !containsSquareCoordinate(coordinate)			
	 */
	@Raw
	protected void resolveConnectionToNeighbours(Coordinate coordinate) throws IllegalArgumentException{
		if(!isValidCoordinate(coordinate) || !containsSquareCoordinate(coordinate))
			throw new IllegalArgumentException();
		Map<Direction, Coordinate> neighboringCoordinates = coordinate.getNeighboringCoordinates();
		Set<Direction> directions = neighboringCoordinates.keySet();
		for(Direction direction: directions) {
			if(containsSquareCoordinate(neighboringCoordinates.get(direction)))
				connectSquareToNeighbor(getSquare(coordinate), direction, getSquare(neighboringCoordinates.get(direction)));
		}
	}
	
	/**
	 * Connect the given square to the given neighbor by the given direction
	 * 
	 * @param 	square
	 * 			The square to be connected.
	 * @param 	direction
	 * 			The direction from square to neighbor
	 * @param 	neighbor
	 * 			the other square taking part in the connection.
	 * @post	square.getBorderingSquare(direction) == neighbor
	 * @post	neighbor.getBorderingSquare(direction) == square
	 * @throws 	IllegalArgumentException
	 * 			| square == null || direction == null || neighbor == null
	 * @throws	IllegalArgumentException
	 * 			If connecting the two squares results in the breaking a class invariable of one of the given squares.
	 */
	@Raw
	protected static void connectSquareToNeighbor(Square square, Direction direction, Square neighbor) throws IllegalArgumentException{
		if(square == null || direction == null || neighbor == null)
			throw new IllegalArgumentException();
		if(square instanceof TransparentSquare){
			if(square.getBorder(direction) instanceof Door) {
				neighbor.connectToObstacle(direction.getOppositeDirection(), (Door)square.getBorder(direction));
			}
			if(square.getBorder(direction) == null) {
				if(neighbor.isValidDirectionToDelete(direction.getOppositeDirection())){
					square.connectToSquare(direction, neighbor);
				}
				else {
					throw new IllegalArgumentException();
				}
			}
				
		}
		else if(neighbor instanceof TransparentSquare){
			if(neighbor.getBorder(direction.getOppositeDirection()) instanceof Door) {
				square.connectToObstacle(direction, (Door)neighbor.getBorder(direction.getOppositeDirection()));
			}
			if(neighbor.getBorder(direction.getOppositeDirection()) == null) {
				if(square.isValidDirectionToDelete(direction)){
					square.connectToSquare(direction, neighbor);
				}
				else {
					throw new IllegalArgumentException();
				}
			}
		}
		else if(neighbor.getBorder(direction.getOppositeDirection()) instanceof Wall){
			square.connectToObstacle(direction, (Obstacle)neighbor.getBorder(direction.getOppositeDirection()));
		}
		else if(square.getBorder(direction) instanceof Wall) {
			neighbor.connectToObstacle(direction.getOppositeDirection(), (Obstacle)square.getBorder(direction));
		}
		else if(neighbor.getBorder(direction.getOppositeDirection()) instanceof Door){
			square.connectToObstacle(direction, (Obstacle)neighbor.getBorder(direction.getOppositeDirection()));
		}
		else if(square.getBorder(direction) instanceof Door){
			neighbor.connectToObstacle(direction.getOppositeDirection(), (Obstacle)square.getBorder(direction));
		}
		else {
			square.connectToSquare(direction, neighbor);
		}
	}
	
}